FeatureUnionを使って機械学習パイプライン内で複数の変換結果を連結する
データアナリティクス事業本部の鈴木です。
今回は、sklearn.pipelineモジュールから、同一のカラムに異なるtransformerを適用し、その結果を結合する際に利用できるFeatureUnionを紹介します。
FeatureUnionとは
入力データに対して、複数のtransformerを適用し、結果をaxis=1方向に連結します。
pipeline.Pipelineでは変換を適用する順序を定義しましたが、FeatureUnionはパイプライン内で作成する特徴をまとめる役割をします。
準備
検証した環境
- コンテナ:jupyter/datascience-notebook
- scikit-learn:0.24.2
データの作成
今回は、sklearn.datasetsモジュールのアイリスデータセットを使います。アイリスデータセットは言わずと知れた有名なデータセットで、3種のアヤメの情報が合計150サンプル分入っています。
動作検証に使えるよう、アイリスデータをデータフレームに変換します。
import numpy as np import pandas as pd from sklearn import datasets # seedの固定 np.random.seed(42) # アヤメのデータをsklearnから読み出す。 iris = datasets.load_iris() # pandasデータフレームに変換する。 # カラム名にスペースが入っていたので、扱いやすい名前に変えた。 feature_names = ["sepal_length", "sepal_width", "petal_length", "petal_width"] df_iris = pd.DataFrame(iris["data"], columns=feature_names) # 各行が何の種類なのか取り出す。 df_iris['species'] = iris.target_names[iris["target"]]
今回は、FeatureUnionの利用例として、欠損値がある際に、以下のような前処理を行うことを考えます。
- 欠損値を中央値で埋める。
- 欠損値があったことを表す特徴量を追加する。
アイリスデータには欠損値がないため、検証用にpetal_widthカラムに欠損値を手作りします。
# 10%をランダムにNoneに置き換える。 df_iris["petal_width"] = [item if np.random.rand() < 0.9 else None for item in df_iris["petal_width"]]
欠損値がランダムに生み出されているので、上記の前処理の意義はあまりありませんが、今回は例ということでご容赦ください。
意図通り加工ができていることを確認します。petal_widthカラムには12レコード分の欠損値があることが分かります。
df_iris.info() ## <class 'pandas.core.frame.DataFrame'> ## RangeIndex: 150 entries, 0 to 149 ## Data columns (total 5 columns): ## # Column Non-Null Count Dtype ## --- ------ -------------- ----- ## 0 sepal_length 150 non-null float64 ## 1 sepal_width 150 non-null float64 ## 2 petal_length 150 non-null float64 ## 3 petal_width 138 non-null float64 ## 4 species 150 non-null object ## dtypes: float64(4), object(1) ## memory usage: 6.0+ KB
後から前処理結果と比較しやすいよう、最初の10行を確認しておきます。
df_iris["petal_width"].head(10) ## 0 0.2 ## 1 0.2 ## 2 0.2 ## 3 0.2 ## 4 0.2 ## 5 NaN ## 6 0.3 ## 7 0.2 ## 8 NaN ## 9 0.1 ## Name: petal_width, dtype: float64
やってみる
FeatureUnionの動作確認
まずは簡単な例から確認します。以下の変換を行うFeatureUnionのインスタンスを使り、petal_widthカラムに適用します。
- 欠損値を中央値で埋める。
- 欠損値があったことを表す特徴量を追加する。
from sklearn.impute import MissingIndicator from sklearn.impute import SimpleImputer from sklearn.pipeline import FeatureUnion # petal_widthカラムの欠損値を中央値で埋め、さらに欠損値の有無を表す特徴を作成する。 petal_width_union = FeatureUnion([("SimpleImputer", SimpleImputer(strategy='median')), ("MissingIndicator", MissingIndicator()) ]) # petal_widthカラムを変換するためにColumnTransformerを作成する。 ct = ColumnTransformer( transformers=[ ('petal_width_transformer', petal_width_union, ["petal_width"]) ]) # 変換を行う。 ct.fit_transform(df_iris) ## array([[0.2, 0. ], ## [0.2, 0. ], ## [0.2, 0. ], ## [0.2, 0. ], ## [0.2, 0. ], ## [1.3, 1. ], ## [0.3, 0. ], ## [0.2, 0. ], ## [1.3, 1. ], ## [0.1, 0. ], ## ...
fit_transformの結果から、期待した変換ができていることを確認できます。
make_unionを使うことで、各ステップに名前を付けずにFeatureUnionを構築することも可能です。
from sklearn.pipeline import make_union from sklearn import set_config set_config(display='diagram') make_union(SimpleImputer(strategy='median'), MissingIndicator())
パイプラインの作成
次に、機械学習パイプラインに組み込んだ例も試してみます。
- petal_widthカラムには2種類の変換を行う。
- 欠損値を中央値で埋め、標準化する。
- 欠損値があったことを表す特徴量を追加する。
- petal_widthカラム以外は標準化する。
from sklearn.compose import ColumnTransformer from sklearn.compose import make_column_selector from sklearn.experimental import enable_hist_gradient_boosting from sklearn.ensemble import HistGradientBoostingClassifier from sklearn.impute import MissingIndicator from sklearn.impute import SimpleImputer from sklearn.pipeline import FeatureUnion from sklearn.pipeline import Pipeline from sklearn.preprocessing import StandardScaler # petal_widthカラム用の変換 petal_width_transformer = Pipeline(steps=[ ('petal_width_imputer', SimpleImputer(strategy='median')), ('petal_width_scaler', StandardScaler())]) # petal_widthカラム用の変換と追加する特徴量用の変換のFeatureUnion union = FeatureUnion([("petal_width_transformer", petal_width_transformer), ("petal_width_missing_indicator", MissingIndicator())]) # 各カラムに適用する変換を定義 ct = ColumnTransformer( transformers=[ ('num_with_null', union, ["petal_width"]), ('num_without_null', StandardScaler(), ["sepal_length", "sepal_width", "petal_length"]) ]) # 全体のパイプラインの作成 pipe = Pipeline([("column_transformer", ct), ("Classifier", HistGradientBoostingClassifier())])
ダイアグラムは以下のようになります。
from sklearn import set_config set_config(display='diagram') pipe
参考までにfit
とscore
を実行し、学習と推論ができることを確認しておきます。
from sklearn.model_selection import train_test_split # 訓練・テストデータの作成 X = df_iris.drop("species", axis=1) y = df_iris["species"] X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=42) # 学習 pipe.fit(X_train, y_train) # テストデータに対するaccuracyを計算する。 pipe.score(X_test, y_test) ## 1.0
最後に
FeatureUnionを使って、入力した一つのカラムに対して複数の変換を行う方法を紹介しました。ColumnTransformerと組み合わせると、かなり複雑な機械学習パイプラインを構築することができるようになりました。
一つのカラムに対して複数の変換を行い、特徴量としたい場合は多いので、参考になれば幸いです。